1. 为什么需要 App Router?(解决了什么问题)
在 App Router 之前,Next.js 使用的是 Pages Router(页面路由)。尽管 Pages Router 非常成功和易于上手,但它也存在一些局限性:
- 布局(Layout)的限制:在 Pages Router 中,实现嵌套布局(例如,一个 dashboard 页面,其侧边栏和顶部导航栏保持不变,只有主内容区域变化)通常需要一些变通方法,比如在
_app.js
中使用getLayout
属性,不够直观和灵活。 - 数据获取的僵化:数据获取与页面强绑定,主要通过
getServerSideProps
,getStaticProps
, 和getStaticPaths
这几个特定的函数。你无法在组件层级灵活地获取数据,导致所有数据都必须在页面级别加载,有时会造成瀑布流(waterfall)加载问题。 - 客户端 JavaScript 过多:默认情况下,Pages Router 中的页面组件最终都会在客户端进行水合(hydrate),即使页面大部分是静态的。这意味着随着应用变大,发送到客户端的 JavaScript 包体积也会随之增长,影响初始加载性能。
- 状态管理的复杂性:在页面切换时,全局状态(如
_app.js
中的 state)会保留,但布局本身可能会被重新渲染,状态管理不够优雅。
App Router 的设计初衷就是为了解决以上这些痛点。
2. App Router 的核心概念
要理解 App Router,必须先掌握以下几个核心概念:
a. React Server Components (RSC)
这是 App Router 的基石。
- 什么是 RSC?:这是一种新型的 React 组件,它 只在服务器上执行和渲染。它的渲染结果(一种特殊的 JSON 格式,不是 HTML)被流式传输到客户端,客户端 React 能够理解并用它来更新 DOM。
- 关键优势:
- 零客户端包体积:默认情况下,Server Components 的代码不会被打包进客户端的 JavaScript 文件中。这意味着你的组件,即使包含大量依赖(如
lodash
,date-fns
),也不会增加用户的下载负担。 - 直接访问后端资源:由于它们在服务器上运行,你可以直接在组件中执行数据库查询、文件系统操作或调用内部 API,而无需创建额外的 API 路由。这极大地简化了数据获取。
- 安全性:敏感数据和逻辑(如 API 密钥、数据库凭证)可以安全地保留在服务器上,永远不会泄露到客户端。
- 自动代码分割:Next.js 会自动按组件进行代码分割,你无需手动配置。
- 零客户端包体积:默认情况下,Server Components 的代码不会被打包进客户端的 JavaScript 文件中。这意味着你的组件,即使包含大量依赖(如
b. 客户端组件 (Client Components)
为了实现交互性(如点击事件、状态管理 useState
, useEffect
等),你需要使用客户端组件。
- 如何定义?:在文件的顶部添加
"use client";
指令,即可将该文件及其导入的所有组件标记为客户端组件。 - 黄金法则:在 Next.js 的 App Router 中,所有组件默认都是 Server Components。只有当你需要使用 Hooks、事件监听器等浏览器端才有的功能时,才将组件转换为 Client Components。尽量将客户端逻辑下沉到应用的“叶子”组件中,以保持大部分应用是 Server Components。
c. 基于文件系统的路由(但以文件夹为中心)
App Router 依然使用文件系统来定义路由,但规则有所改变。
- 文件夹即路由:每个文件夹都定义了一个 URL 段。
- 特殊文件约定:UI 是通过在文件夹内创建具有特定名称的文件来构建的。
3. 特殊文件约定 (Special File Conventions)
这是 App Router 的“API”,你通过创建这些文件来构建应用的各个部分。
-
app/
目录:所有 App Router 的相关文件都必须放在app
目录下。 -
page.tsx
/page.js
: 定义一个路由段的 唯一 UI。这是用户访问该 URL 时看到的主要内容。一个文件夹里必须有一个page.tsx
才能让这个路由可以被公开访问。 -
layout.tsx
/layout.js
: 定义一个路由段及其 子路由共享的 UI。layout
会包裹page
或子layout
。最顶层的app/layout.tsx
是根布局,必须包含<html>
和<body>
标签。布局在页面切换时 不会 重新渲染,状态会得以保留。 -
template.tsx
/template.js
: 与layout
类似,也是包裹子路由的共享 UI。但关键区别在于,每次导航时template
都会创建一个新的实例,这意味着它的状态不会被保留,并且其中的useEffect
等会重新执行。适用于需要每次进入都执行动画或逻辑的场景。 -
loading.tsx
/loading.js
: 一个自动的 加载 UI。当page.tsx
或其子组件正在获取数据时,Next.js 会使用 React Suspense 自动将loading.tsx
的内容展示出来。这极大地简化了加载状态的管理。 -
error.tsx
/error.js
: 一个自动的 错误 UI。当该路由段或其子组件中抛出错误时,Next.js 会使用 React Error Boundary 自动捕获并渲染error.tsx
的内容。它提供了一个reset
函数,可以尝试重新渲染。 -
not-found.tsx
/not-found.js
: 当你在组件中调用notFound()
函数或者访问一个不存在的路由时,会渲染这个 UI。 -
route.ts
/route.js
: 用于创建 API 端点,替代了 Pages Router 中的pages/api
目录。你可以在其中导出GET
,POST
,PUT
,DELETE
等函数。
示例文件结构:
app/
├── layout.tsx # 根布局 (<html>, <body>)
├── page.tsx # 首页 (/)
└── dashboard/
├── layout.tsx # Dashboard 共享布局 (例如带侧边栏)
├── page.tsx # Dashboard 主页 (/dashboard)
├── loading.tsx # /dashboard 及其子路由的加载 UI
├── error.tsx # /dashboard 及其子路由的错误 UI
└── settings/
├── page.tsx # 设置页 (/dashboard/settings)
渲染顺序:对于 /dashboard/settings
,组件的嵌套关系是:
app/layout.tsx
-> app/dashboard/layout.tsx
-> app/dashboard/settings/page.tsx
4. App Router 的关键特性与优势
a. 简化的数据获取
你可以直接在 Server Component 中使用 async/await
来获取数据。
// app/posts/[id]/page.tsx - 这是一个 Server Component
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`);
return res.json();
}
export default async function PostPage({ params }) {
const post = await getPost(params.id); // 直接在组件中获取数据
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
Next.js 扩展了原生的 fetch
API,为其增加了自动缓存和重新验证的控制能力,你可以通过 { cache: 'no-store' }
或 { next: { revalidate: 3600 } }
来精细控制缓存策略。
b. 流式渲染与 Suspense (Streaming & Suspense)
这是提升用户体验的利器。当一个页面包含多个需要加载数据的组件时,服务器不必等待所有数据都准备好才开始发送响应。
- 服务器首先发送页面的静态部分(如布局和不需要数据的组件)。
- 对于正在加载数据的组件,服务器会发送一个占位符(由
loading.tsx
定义)。 - 一旦某个组件的数据准备就绪,服务器会通过同一个请求流,将该组件的渲染结果发送到客户端。
- 客户端 React 将接收到的内容无缝地填充到对应的位置。
用户会先看到页面的骨架,然后内容会逐步填充进来,大大减少了可感知的加载时间。
c. 嵌套布局与路由组 (Nested Layouts & Route Groups)
- 嵌套布局:如上文文件结构所示,
layout.tsx
文件天然支持嵌套,子路由的布局会自动包裹在父路由的布局之内,非常直观。 - 路由组:如果你想组织文件结构,但又不希望文件夹名称影响 URL,可以使用括号
()
。例如app/(marketing)/about/page.tsx
的 URL 仍然是/about
。这对于按功能或团队划分代码非常有用。
d. 更高级的路由模式
- 并行路由 (Parallel Routes):允许你在同一个视图中同时渲染一个或多个页面。这对于实现复杂的仪表盘(如一个主内容区和一个侧边分析面板)非常有用。通过
@folder
语法实现。 - 拦截路由 (Intercepting Routes):允许你从一个路由中“拦截”并显示另一个路由的内容,而 URL 保持不变。常用于在当前页面上打开一个模态框(Modal),但如果直接刷新页面,则会显示模态框的独立页面。通过
(..)
或(...)
语法实现。
5. App Router vs. Pages Router (快速对比)
特性 | App Router (app/) | Pages Router (pages/) |
---|---|---|
默认组件 | React Server Components (RSC) | 客户端组件 |
路由文件 | page.tsx | index.tsx 或 [slug].tsx 等 |
布局 | layout.tsx (原生支持嵌套) | _app.js + getLayout 模式 |
数据获取 | async/await 在组件中, fetch 扩展 | getServerSideProps , getStaticProps |
API 路由 | route.ts | pages/api/*.ts |
加载状态 | loading.tsx (自动, 基于 Suspense) | 手动实现 |
错误处理 | error.tsx (自动, 基于 Error Boundary) | 手动实现或使用 _error.js |
服务端渲染 | 支持 Streaming, 默认服务器中心 | 支持 SSR/SSG,默认客户端水合 |
总结
Next.js 的 App Router 是一次重大的架构升级,它将 服务器优先 的理念贯彻到底。
- 对于开发者:它提供了更直观的布局系统、更灵活的数据获取方式,并简化了加载和错误状态的管理。虽然初期有一定的学习曲线(尤其是理解 Server/Client Components 的区别),但长期来看能显著提高开发效率和代码质量。
- 对于用户:通过 Server Components 和流式渲染,用户可以体验到更快的页面加载速度和更流畅的交互,即使是在网络状况不佳的情况下。
虽然 Pages Router 依然被支持,但 App Router 代表了 Next.js 的未来方向。对于新项目,强烈建议从 App Router 开始。
高级特性
1. 并行路由 (Parallel Routes)
是什么?
并行路由允许你在同一个视图(由同一个 layout.tsx
控制)中,同时渲染一个或多个独立的、可独立导航的“页面”。它们就像是同一个布局下的多个“子视图”或“槽位 (slots)”。
为什么有用? 想象一个复杂的仪表盘(Dashboard)。主内容区旁边可能有一个团队活动信息流,底部还有一个分析图表。这三个区域的数据来源、加载状态、甚至交互逻辑都是独立的。
- 传统方法:你需要在顶层 Dashboard 页面
fetch
所有数据,然后通过 props 层层传递下去,非常笨拙,并且会产生请求瀑布。 - 并行路由方法:你可以将每个区域定义为一个独立的并行路由。它们会并行加载数据,互不干扰。主布局只需要提供“插槽”,Next.js 会自动将渲染好的内容填入。
如何实现?
通过在文件夹名前加上 @
符号来定义一个槽 (slot)。
示例文件结构:
app/
└── dashboard/
├── @analytics/ # "analytics" 槽
│ └── page.tsx
├── @team/ # "team" 槽
│ └── page.tsx
├── layout.tsx # 共享布局
└── page.tsx # 主要内容 (隐式的 @children 槽)
app/dashboard/layout.tsx
的实现:
布局组件会通过 props
接收到所有定义的槽。
// app/dashboard/layout.tsx
export default function DashboardLayout({ children, team, analytics }) {
// `children` 是 page.tsx
// `team` 是 @team/page.tsx
// `analytics` 是 @analytics/page.tsx
return (
<div>
{children}
<div style={{ display: 'flex', gap: '1rem' }}>
{team}
{analytics}
</div>
<div/>
);
}
关键点:
- 独立性:每个槽 (
@analytics
,@team
) 都有自己的加载和错误状态。你可以在@analytics/
文件夹下添加loading.tsx
和error.tsx
,它们只对该槽生效。 default.tsx
:当 Next.js 无法根据当前 URL 确定某个槽中应渲染什么内容时(例如,在页面刷新后),它会渲染该槽下的default.tsx
文件。这是一个很好的回退机制。
2. 拦截路由 (Intercepting Routes)
是什么? 拦截路由允许你在当前布局中“拦截”并显示一个路由的内容,同时浏览器的 URL 可以选择性地更新。用户感觉像是在当前页面上打开了一个模态框(Modal)或进行了内容预览,但这个模态框本身也是一个可以被直接访问和刷新的独立页面。
为什么有用? 最经典的案例是图片库:
- 用户在图片墙页面 (
/gallery
)。 - 点击一张图片,弹出一个模态框显示大图。URL 变为
/photo/123
,但背景仍然是图片墙。 - 用户关闭模态框,URL 回到
/gallery
。 - 如果用户直接访问或刷新
/photo/123
,他们会看到一个独立的、包含这张图片的完整页面,而不是模态框。
如何实现?
通过 (.)
、(..)
和 (...)
这样的相对路径约定。
(.)
:匹配同一层级的路由。(..)
:匹配上一层级的路由。(..)(..)
:匹配上两层级的路由。(...)
:从根app/
目录开始匹配。
示例文件结构(结合并行路由实现模态框):
app/
├── @modal/ # 一个用于显示模态框的并行路由槽
│ └── (..)photo/[id]/ # 拦截规则文件夹
│ └── page.tsx # 模态框中显示的内容
├── gallery/
│ └── page.tsx # 图片墙页面,包含指向 /photo/[id] 的链接
└── photo/
└── [id]/
└── page.tsx # 图片的独立页面
└── layout.tsx # 根布局,包含 @modal 槽
app/layout.tsx
export default function RootLayout({ children, modal }) {
return (
<html>
<body>
{children}
{modal} {/* 当拦截路由被触发时,@modal/page.tsx 的内容会渲染在这里 */}
</body>
</html>
);
}
工作流程:
- 当你在
/gallery
页面点击一个<Link href="/photo/123">
。 - Next.js 路由器检测到这个导航。
- 它发现
app/@modal/(..)photo/[id]
这个拦截路由匹配photo/[id]
(..
表示从gallery
的父级,即app
开始查找)。 - 于是,它不会导航到
photo/[id]/page.tsx
,而是渲染app/@modal/(..)photo/[id]/page.tsx
的内容到@modal
槽中,并更新 URL。 - 如果你直接刷新
/photo/123
,没有发生拦截,Next.js 会正常渲染app/photo/[id]/page.tsx
。
3. 服务端操作 (Server Actions)
是什么? Server Actions 是一些可以直接从客户端组件(或服务端组件)中调用的异步函数,它们 只在服务器上执行。这是一种用于数据变更(Mutations)的内置解决方案,无需手动创建 API 端点。
为什么有用? 极大地简化了表单提交和数据更新的逻辑。
- 告别 API 路由样板代码:你不再需要为每个表单创建一个
POST /api/form
路由。 - 渐进增强:绑定到
<form>
的 Server Action 即使在 JavaScript 被禁用的情况下也能工作。 - 简化数据刷新:可以在 Action 内部直接调用
revalidatePath
或revalidateTag
来清除缓存并刷新页面数据,实现了无缝的 UI 更新。
如何实现?
在函数顶部或文件顶部添加 "use server";
指令。
示例:一个简单的待办事项表单
// app/actions.ts (推荐将 Actions 放在单独的文件中)
"use server";
import { revalidatePath } from 'next/cache';
import { db } from './lib/db'; // 假设这是你的数据库实例
export async function addTodo(formData: FormData) {
const todo = formData.get("todo") as string;
if (!todo) return;
await db.todos.create({ text: todo });
// 关键:当 action 执行成功后,让所有访问 /todos 的页面数据重新生效
revalidatePath("/todos");
}
// app/todos/page.tsx (可以是 Server Component)
import { addTodo } from "@/app/actions";
import { db } from '@/app/lib/db';
export default async function TodosPage() {
const todos = await db.todos.findMany();
return (
<div>
<form action={addTodo}> {/* 直接绑定 Server Action */}
<input type="text" name="todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</div>
);
}
4. 精细的缓存与数据重新验证 (Caching & Revalidation)
是什么?
App Router 对原生的 fetch
API 进行了扩展,赋予了它强大的缓存控制能力。你可以决定数据的缓存策略,并按需(On-demand)或按时(Time-based)使其失效。
为什么有用? 这是性能优化的核心。你可以灵活地在静态(SSG)、增量静态再生(ISR)和动态(SSR)渲染模式之间切换,甚至在单个页面上混合使用。
如何实现?
缓存策略 (fetch
选项):
- 默认 (SSG/ISR):
fetch('...')
或fetch('...', { cache: 'force-cache' })
。数据在构建时被获取和缓存。 - 动态 (SSR):
fetch('...', { cache: 'no-store' })
。每次请求都重新获取数据。 - 增量静态再生 (ISR):
fetch('...', { next: { revalidate: 3600 } })
。数据被缓存,但在 3600 秒后第一次请求时会重新获取。
按需重新验证 (On-demand Revalidation):
-
基于标签 (Tag-based):
- 在 fetch 时给数据打上标签:
fetch('...', { next: { tags: ['posts', 'user-profile'] } });
- 在 Server Action 或 API 路由中使标签失效:
// 在 Server Action 中 import { revalidateTag } from 'next/cache'; revalidateTag('posts');
这会使所有打了
posts
标签的fetch
数据缓存失效。 - 在 fetch 时给数据打上标签:
-
基于路径 (Path-based):
import { revalidatePath } from 'next/cache'; revalidatePath('/blog/[slug]', 'page'); // 精确重新验证单个页面 revalidatePath('/blog', 'layout'); // 重新验证布局和页面
这些高级特性共同构成了 App Router 的强大能力,它们将前端开发从“构建客户端单页应用”的思维模式,转变为“在服务器上组合可交互的 UI 片段”,从而在性能、开发体验和应用架构上都带来了质的飞跃。
约定规范
核心原则:基于文件系统,但以“约定”为中心
与 Pages Router 不同(文件名直接映射到 URL),App Router 的核心是 在特定文件夹内创建具有特殊名称的文件。文件夹定义了 URL 段,而这些特殊文件则定义了该 URL 段的 UI、逻辑或元数据。
所有 App Router 的文件和文件夹都必须位于项目根目录下的 app/
目录中。
1. 路由定义 (Route Definition)
a. 文件夹 (Folders)
- 作用:定义路由段 (Route Segments)。
- 示例:
app/dashboard/settings
会创建一个 URL 路径/dashboard/settings
。- 嵌套文件夹会创建嵌套的路由。
b. page.tsx
/ page.js
- 作用:定义路由的公开 UI。这是让一个 URL 路径可以被用户直接访问的必要文件。
- 行为:当用户访问
app/dashboard
对应的/dashboard
URL 时,app/dashboard/page.tsx
的内容会被渲染。 - 关键点:一个文件夹(路由段)如果没有
page.tsx
,它本身就不是一个可访问的页面,但它可以包含子路由。
2. UI 嵌套与布局 (UI Nesting & Layouts)
a. layout.tsx
/ layout.js
- 作用:定义一个 共享的、持久化的 UI,它会包裹其所在的路由段以及所有子路由段。
- 行为:
- 布局接收一个
children
prop,代表子layout
或page
。 - 在导航切换时,
layout
组件的实例和状态(state)会被保留,不会重新渲染。 - 根布局 (
app/layout.tsx
) 是必需的,并且必须包含<html>
和<body>
标签。
- 布局接收一个
- 示例:
app/dashboard/layout.tsx
会包裹app/dashboard/page.tsx
、app/dashboard/settings/page.tsx
等所有/dashboard
下的页面。
b. template.tsx
/ template.js
- 作用:与
layout
类似,也是一个包裹子组件的共享 UI。 - 行为:
- 关键区别:每次导航到或离开其作用域的路由时,
template
都会创建一个 新的实例。 - 这意味着它的状态不会被保留,并且
useEffect
、useState
等 Hooks 会重新执行。
- 关键区别:每次导航到或离开其作用域的路由时,
- 适用场景:
- 需要实现进入/退出动画(例如使用 Framer Motion)。
- 依赖
useEffect
来执行某些每次进入页面都需要运行的逻辑(如页面浏览量跟踪)。 - 重置某些特定状态。
3. 加载与错误处理 (Loading & Error Handling)
a. loading.tsx
/ loading.js
- 作用:创建一个 即时的加载 UI (Instant Loading UI)。
- 行为:Next.js 会自动使用 React Suspense 包裹你的
page.tsx
和其子组件。当这些组件正在进行异步操作(如fetch
数据)时,Next.js 会自动渲染同级或上级最近的loading.tsx
文件作为后备 (fallback)。 - 优势:极大地简化了加载状态管理,并支持流式渲染(Streaming)。
b. error.tsx
/ error.js
- 作用:创建一个 错误 UI 边界 (Error UI Boundary)。
- 行为:
- Next.js 会自动使用 React Error Boundary 包裹你的
page.tsx
和其子组件。 - 当这些组件在渲染过程中抛出错误时,Next.js 会捕获该错误并渲染同级或上级最近的
error.tsx
。 error.tsx
组件 必须是客户端组件 ("use client";
),因为它需要处理事件(如重试)。它会接收error
和reset
两个 props。reset
是一个函数,调用它可以尝试重新渲染出错的组件。
- Next.js 会自动使用 React Error Boundary 包裹你的
- 优势:隔离错误,防止单个组件的错误导致整个应用崩溃。
c. global-error.tsx
/ global-error.js
- 作用:这是
error.tsx
的一个特殊变体,专门用于捕获并处理 根layout.tsx
中的错误。 - 行为:由于根布局的错误会导致整个应用无法渲染,
global-error.tsx
提供了一个最终的保障。它会替换整个根布局来显示错误信息。
4. 路由处理与特殊页面 (Route Handling & Special Pages)
a. not-found.tsx
/ not-found.js
- 作用:定义一个 “未找到” UI。
- 行为:当
notFound()
函数在组件中被调用,或者用户访问了一个不存在的 URL 时,Next.js 会渲染最近的not-found.tsx
文件。 notFound()
函数:可以在 Server Component 的数据获取逻辑中使用。如果fetch
返回 404,你可以调用notFound()
来中断渲染并显示 404 页面。
b. route.ts
/ route.js
- 作用:创建 API 端点 (API Endpoints),替代了 Pages Router 中的
pages/api
。 - 行为:你可以在此文件中导出与 HTTP 方法同名的函数,如
GET
,POST
,PUT
,DELETE
等。 - 示例:
app/api/users/route.ts
中导出的GET
函数将处理GET /api/users
请求。
5. 路由组织与高级模式 (Route Organization & Advanced Patterns)
a. 路由组 (Route Groups) - (folderName)
- 作用:组织文件结构,但不影响 URL 路径。
- 行为:用括号包裹的文件夹名会被路由器忽略。
- 示例:
app/(marketing)/about/page.tsx
对应的 URL 是/about
,而不是/(marketing)/about
。 - 适用场景:
- 按功能或团队划分代码。
- 为不同的路由段创建不同的根布局。例如,
app/(marketing)/layout.tsx
和app/(app)/layout.tsx
可以为网站的市场部分和应用部分提供完全不同的顶层布局。
b. 动态路由段 (Dynamic Segments) - [folderName]
- 作用:匹配动态的 URL 参数。
- 行为:与 Pages Router 类似。
- 示例:
app/blog/[slug]/page.tsx
会匹配/blog/hello-world
、/blog/another-post
等。slug
参数会通过 props 传递给page
,layout
等组件。
generateStaticParams
函数:可以与动态路由段一起使用,在构建时预渲染一组特定的路径(SSG)。
c. 捕获所有段 (Catch-all Segments) - [...folderName]
- 作用:匹配从该点开始的所有后续 URL 段。
- 示例:
app/shop/[...slug]/page.tsx
会匹配/shop/a
、/shop/a/b
、/shop/a/b/c
等。slug
参数会是一个数组,如['a', 'b', 'c']
。
d. 可选捕获所有段 (Optional Catch-all Segments) - [[...folderName]]
- 作用:与 Catch-all 类似,但它也匹配没有后续段的路径。
- 示例:
app/shop/[[...slug]]/page.tsx
会匹配/shop
以及/shop/a
、/shop/a/b
等。
e. 并行路由槽 (Parallel Route Slots) - @folderName
- 作用:在同一个布局中定义可以独立渲染的“槽位”。
- 示例:
app/dashboard/@team/page.tsx
定义了一个名为team
的槽。父级布局app/dashboard/layout.tsx
会通过 props 接收到team
的渲染结果。
f. 拦截路由 (Intercepting Routes) - (.)
, (..)
- 作用:在当前布局中显示另一个路由的内容,通常用于模态框。
- 示例:
app/@modal/(.)photo/[id]/page.tsx
会拦截到同一层级的photo/[id]
导航。
6. 元数据文件 (Metadata Files)
App Router 引入了一套基于文件的元数据 API。
favicon.ico
,apple-icon.jpg
,icon.jpg
:放置在app
目录的根部,用于定义应用图标。opengraph-image.jpg
,twitter-image.jpg
:放置在任何路由段中,为该路由生成社交媒体分享卡片图片。sitemap.ts
/robots.ts
:放置在app
根目录,用于动态生成sitemap.xml
和robots.txt
。
总结表格
文件/文件夹约定 | 目的 |
---|---|
app/ | App Router 的根目录 |
folder | 创建一个 URL 路由段 |
page.tsx | 创建一个路由段的公开 UI |
layout.tsx | 创建持久化的共享 UI |
template.tsx | 创建重新渲染的共享 UI |
loading.tsx | 创建加载 UI (Suspense Boundary) |
error.tsx | 创建错误 UI (Error Boundary) |
not-found.tsx | 创建 404 Not Found UI |
route.ts | 创建 API 端点 |
(folder) | 路由组,不影响 URL |
[folder] | 动态路由段 |
[...folder] | 捕获所有路由段 |
[[...folder]] | 可选的捕获所有路由段 |
@folder | 并行路由槽 |
(.) , (..) | 拦截路由 |
理解并熟练运用这些命名约定,是掌握 Next.js App Router 的关键所在。
例子
一、路由组 (Route Groups) - (folderName)
路由组的核心价值在于 组织代码结构而不影响最终的 URL。这看似简单,但却能解决许多实际开发中的架构问题。
1. 为应用的不同部分创建独立的布局
这是路由组最经典、最强大的应用场景。一个复杂的应用通常有几个完全不同的区域,比如:
- 市场/营销页面 (
/
,/about
,/pricing
):通常有公共的页头、页脚,设计风格偏向展示。 - 应用主功能页面 (
/dashboard
,/settings
):通常需要用户登录,有侧边栏、用户菜单等复杂的交互布局。 - 认证页面 (
/login
,/signup
):通常是居中的、极简的布局,不应包含主应用的导航栏。
没有路由组的困境:
所有页面都共享同一个根布局 (app/layout.tsx
)。你不得不在根布局中写复杂的条件逻辑来判断当前路由,然后决定渲染哪个子布局。这非常混乱且难以维护。
// 👎 不推荐的写法:在根布局中写条件逻辑
export default function RootLayout({ children }) {
const pathname = usePathname(); // 需要客户端组件才能获取路径
if (pathname.startsWith('/dashboard')) {
return <DashboardLayout>{children}</DashboardLayout>;
} else if (pathname === '/login') {
return <AuthLayout>{children}</AuthLayout>;
} else {
return <MarketingLayout>{children}</MarketingLayout>;
}
}
使用路由组的优雅解决方案:
你可以创建多个路由组,每个组都有自己的顶层 layout.tsx
。
文件结构:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # URL: /about
│ ├── pricing/
│ │ └── page.tsx # URL: /pricing
│ └── layout.tsx # 只对 (marketing) 组生效的布局
├── (app)/
│ ├── dashboard/
│ │ └── page.tsx # URL: /dashboard
│ ├── settings/
│ │ └── page.tsx # URL: /settings
│ └── layout.tsx # 只对 (app) 组生效的布局,可以包含登录校验
├── (auth)/
│ ├── login/
│ │ └── page.tsx # URL: /login
│ └── layout.tsx # 只对 (auth) 组生效的布局
└── layout.tsx # 全局根布局 (仅包含 <html>, <body>)
优势:
- 关注点分离:每个部分的布局逻辑都封装在自己的
layout.tsx
中。 - 代码清晰:根布局保持干净,只负责最基础的 HTML 结构。
- URL 纯净:
(marketing)
、(app)
等文件夹名不会出现在最终的 URL 中。 - 服务端优先:布局的选择在服务端就已经确定,无需在客户端进行判断。
2. 组织项目文件,便于团队协作
当项目变大时,app
目录可能会变得非常庞大。路由组可以帮助你根据功能、特性或团队来组织文件。
文件结构:
app/
├── (shop)/
│ ├── products/
│ ├── cart/
│ └── checkout/
├── (blog)/
│ ├── [slug]/
│ └── layout.tsx
├── (account)/
│ ├── profile/
│ └── orders/
└── page.tsx
即使这些部分可能共享同一个主布局,这种组织方式也能让开发者快速定位到自己负责的代码区域,提高可维护性。
3. 避免将特定路由段纳入布局
有时,你希望某个页面不应用其父级的布局。路由组可以帮你“跳出”布局的包裹。
场景:
假设 app/dashboard/layout.tsx
提供了一个带侧边栏的布局。但你希望 /dashboard/reports/full
这个页面是全屏的,没有侧边栏。
文件结构:
app/
├── dashboard/
│ ├── (with-sidebar)/ # 把需要侧边栏的页面都放进这个组
│ │ ├── settings/page.tsx
│ │ └── page.tsx
│ │ └── layout.tsx # 带侧边栏的布局
│ │
│ └── reports/
│ └── full/
│ └── page.tsx # 这个页面不会应用 (with-sidebar) 的布局
│
└── layout.tsx # dashboard 的根布局,可以更简单
通过这种方式,你可以精确控制布局的应用范围。